【MediaPipe】Multi Hand Trackingから検出データを抽出した
※ 5/28:必要手順に抜けがありましたので、追記いたしました。 ※ 5/29:本記事の内容はMediaPipeのバージョンはv0.7.4で実行した内容です。5/22にバージョンがv0.7.5に変更になったため、以下の方法では動かない可能性がありますので、ご注意ください。
カフェチームの山本です。
前々回、前回で、Multi Hand Trackingのプログラムとグラフを解析しました。
今回は、Multi Hand TrackingのGraphを変更し、検出結果のデータを抽出します。Graphにデータ出力用のストリームを追加し、受け取る側のプログラムを変更します。また、抽出したデータはProtocol Buffer形式で少し扱いにくいため、C++からシリアライズ化したデータを出力し、PythonでJSON形式に変換します。
(MediaPipeに関連する記事はこちらにまとめてあります。)
Graphに出力ストリームを追加する
Graphからデータを取り出すため、ほしいデータが通っているストリームを、出力ストリームに接続します。今回、取り出す対象のストリームは、結果を描画するMultiHandRendererに入力されているストリーム4つと、MultiHandLandmarkから出力されているストリーム1つです。
今回、出力ストリームに検出結果のストリームを直接接続する方法がわからなかったため、(HelloWorldで利用されていた)PassThroughCalculatorを介して出力するようにしました。各Calculatorで、対象の検出結果ストリームをInputとして指定し、追加した出力ストリームをOutputとして指定します。(もしかしたら、別の方法があるかもしれません)
GraphからPassThroughCalculatorを参照できるようにする
mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxtから参照できるように、BUILDファイルににPassThroughCalculatorへのパスを追加します。
cc_library( name = "multi_hand_desktop_tflite_calculators", deps = [ ":desktop_offline_calculators", "//mediapipe/calculators/util:association_norm_rect_calculator", "//mediapipe/calculators/util:collection_has_min_size_calculator", "//mediapipe/graphs/hand_tracking/subgraphs:multi_hand_detection_cpu", "//mediapipe/graphs/hand_tracking/subgraphs:multi_hand_landmark_cpu", "//mediapipe/graphs/hand_tracking/subgraphs:multi_hand_renderer_cpu", "//mediapipe/calculators/core:pass_through_calculator", ], )
Graphに出力ストリームを接続する
mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxtを以下のように変更します。
- それぞれの検出結果を取り出すための出力ストリームを追加する(9~13行目)
- 出力ストリームに検出結果のストリームを接続する(110~140行目)。
# MediaPipe graph that performs multi-hand tracking on desktop with TensorFlow # Lite on CPU. # Used in the example in # mediapipie/examples/desktop/hand_tracking:multi_hand_tracking_cpu. # Images coming into and out of the graph. input_stream: "input_video" output_stream: "output_video" output_stream: "output_detections" output_stream: "output_landmarks" output_stream: "output_palm_rects" output_stream: "output_hand_rects" output_stream: "output_hand_rects_from_landmarks" # Determines if an input vector of NormalizedRect has a size greater than or # equal to the provided min_size. node { calculator: "NormalizedRectVectorHasMinSizeCalculator" input_stream: "ITERABLE:prev_multi_hand_rects_from_landmarks" output_stream: "prev_has_enough_hands" node_options: { [type.googleapis.com/mediapipe.CollectionHasMinSizeCalculatorOptions] { # This value can be changed to support tracking arbitrary number of hands. # Please also remember to modify max_vec_size in # ClipVectorSizeCalculatorOptions in # mediapipe/graphs/hand_tracking/subgraphs/multi_hand_detection_gpu.pbtxt min_size: 8 } } } # Drops the incoming image if the previous frame had at least N hands. # Otherwise, passes the incoming image through to trigger a new round of hand # detection in MultiHandDetectionSubgraph. node { calculator: "GateCalculator" input_stream: "input_video" input_stream: "DISALLOW:prev_has_enough_hands" output_stream: "multi_hand_detection_input_video" node_options: { [type.googleapis.com/mediapipe.GateCalculatorOptions] { empty_packets_as_allow: true } } } # Subgraph that detections hands (see multi_hand_detection_cpu.pbtxt). node { calculator: "MultiHandDetectionSubgraph" input_stream: "multi_hand_detection_input_video" output_stream: "DETECTIONS:multi_palm_detections" output_stream: "NORM_RECTS:multi_palm_rects" } # Subgraph that localizes hand landmarks for multiple hands (see # multi_hand_landmark.pbtxt). node { calculator: "MultiHandLandmarkSubgraph" input_stream: "IMAGE:input_video" input_stream: "NORM_RECTS:multi_hand_rects" output_stream: "LANDMARKS:multi_hand_landmarks" output_stream: "NORM_RECTS:multi_hand_rects_from_landmarks" } # Caches a hand rectangle fed back from MultiHandLandmarkSubgraph, and upon the # arrival of the next input image sends out the cached rectangle with the # timestamp replaced by that of the input image, essentially generating a packet # that carries the previous hand rectangle. Note that upon the arrival of the # very first input image, an empty packet is sent out to jump start the # feedback loop. node { calculator: "PreviousLoopbackCalculator" input_stream: "MAIN:input_video" input_stream: "LOOP:multi_hand_rects_from_landmarks" input_stream_info: { tag_index: "LOOP" back_edge: true } output_stream: "PREV_LOOP:prev_multi_hand_rects_from_landmarks" } # Performs association between NormalizedRect vector elements from previous # frame and those from the current frame if MultiHandDetectionSubgraph runs. # This calculator ensures that the output multi_hand_rects vector doesn't # contain overlapping regions based on the specified min_similarity_threshold. node { calculator: "AssociationNormRectCalculator" input_stream: "prev_multi_hand_rects_from_landmarks" input_stream: "multi_palm_rects" output_stream: "multi_hand_rects" node_options: { [type.googleapis.com/mediapipe.AssociationCalculatorOptions] { min_similarity_threshold: 0.5 } } } # Subgraph that renders annotations and overlays them on top of the input # images (see multi_hand_renderer_cpu.pbtxt). node { calculator: "MultiHandRendererSubgraph" input_stream: "IMAGE:input_video" input_stream: "DETECTIONS:multi_palm_detections" input_stream: "LANDMARKS:multi_hand_landmarks" input_stream: "NORM_RECTS:0:multi_palm_rects" input_stream: "NORM_RECTS:1:multi_hand_rects" output_stream: "IMAGE:output_video" } # Streams to get results of detection and landmark node { calculator: "PassThroughCalculator" input_stream: "multi_palm_detections" output_stream: "output_detections" } node { calculator: "PassThroughCalculator" input_stream: "multi_hand_landmarks" output_stream: "output_landmarks" } node { calculator: "PassThroughCalculator" input_stream: "multi_palm_rects" output_stream: "output_palm_rects" } node { calculator: "PassThroughCalculator" input_stream: "multi_hand_rects" output_stream: "output_hand_rects" } node { calculator: "PassThroughCalculator" input_stream: "multi_hand_rects_from_landmarks" output_stream: "output_hand_rects_from_landmarks" }
可視化すると、次のようになります。図下側の5つの PassThrough_* と output_* が今回追加した箇所です。(図だけだとわかりにくいと思いますので、Visualizer に上のコードを貼り付けて、マウスオーバなどしながら確認するのがオススメです)
変更前
変更後
プログラムを変更する
Graphを変更したことによって、データを取り出す準備ができました。次にプログラム側を変更します。取り出す検出データの型情報が必要であるため、対応するヘッダファイルを利用します。今回取り出すデータは、Protocol Bufferで定義されているもの(.protoファイル)を、bazelでビルドしてヘッダファイルとします。詳しくはこちらなどをご参照ください。
プログラムからヘッダファイルを参照できるようにする
mediapipe/examples/desktop/demo_run_graph_main.ccから参照するため、同じフォルダのBUILDファイルに以下のパスを追加します。
cc_library( name = "demo_run_graph_main", srcs = ["demo_run_graph_main.cc"], deps = [ "//mediapipe/framework:calculator_framework", "//mediapipe/framework/formats:image_frame", "//mediapipe/framework/formats:image_frame_opencv", "//mediapipe/framework/port:commandlineflags", "//mediapipe/framework/port:file_helpers", "//mediapipe/framework/port:opencv_highgui", "//mediapipe/framework/port:opencv_imgproc", "//mediapipe/framework/port:opencv_video", "//mediapipe/framework/port:parse_text_proto", "//mediapipe/framework/port:status", "//mediapipe/framework/formats:detection_cc_proto", "//mediapipe/framework/formats:landmark_cc_proto", "//mediapipe/framework/formats:rect_cc_proto", ], )
データを取り出して出力する
- 取り出すデータのクラスを定義したヘッダファイルをインクルードする(今回取り出す結果は、Protocol Bufferで定義されているため、bazelがビルドして生成するヘッダファイル(*.pb.h)をインクルードする必要があります)(34~36行目)
- 出力結果を描画した画像を取り出す処理と同様に、出力ストリームに対するポーラをGraphに接続し(99~108行目)、Packetを通して結果を得る(160~179行目)。
- (今回はファイル出力するために)SerializeToString()でバイナリデータをstring形式で取得し、ファイル出力する(182~272行目)
今回のプログラムでは、以下のようなファイルが出力されます。出力されるファイル数は検出された結果によって異なります。
- iLoop=0_detection_j=0.txt
- iLoop=0_detection_j=1.txt
- iLoop=0_handRect_j=0.txt
- iLoop=0_handRect_j=1.txt
- iLoop=0_handRectFromLandmarks_j=0.txt
- iLoop=0_handRectFromLandmarks_j=1.txt
- iLoop=0_inputFrame.jpg
- iLoop=0_landmark_j=0.txt
- iLoop=0_landmark_j=1.txt
- iLoop=0_outputFrame.jpg
- iLoop=0_palmRect_j=0.txt
- iLoop=1_detection_j=0.txt
- ...
検出データを変換する
出力したデータを利用しやすくするために、Pythonで読み込みJSON形式に変換します。まず、Protocol Buffer形式で定義されたクラスを、Pythonで使えるようにするため、Python用にコンパイルします。その後、
Protobuf定義ファイルをPython用にコンパイルする
protocをインストールするため、以下のコマンドを実行します。コードはこちらを参考にしました。
sudo apt-get install autoconf automake libtool curl make g++ unzip -y git clone https://github.com/google/protobuf.git cd protobuf git submodule update --init --recursive ./autogen.sh ./configure make make check sudo make install sudo ldconfig
インストールできてるか確認します。
protoc --version
今回利用するProtobufファイルをコンパイルします。コマンドはこちらを参考にしました。
protoc --python_out=./ ./mediapipe/framework/formats/*.proto protoc --python_out=./ ./mediapipe/framework/formats/annotation/*.proto
./temp/mediapipe/framework/formats内に、Python用にコンパイルされたファイル(*_pb2.py)が生成されます。
JSON形式に変換する
protobufのライブラリが必要であるため、以下のコマンドをWSLで実行します。
pip3 install protobuf
今回使用したコードは以下のようです(雑なプログラムですが、、、)。
- 先程コンパイルしたライブラリをインポートする(1~3行目)
- それぞれの出力の種類ごとに、ファイル名のリストを取得する(今回の主眼ではありません)(20~43行目)
- 各データ形式に対応するクラスでパースし(ParseFromString)、JSON形式に変換し(MessageToJson)、ファイル出力する(46~107行目)
from mediapipe.framework.formats.detection_pb2 import Detection from mediapipe.framework.formats.landmark_pb2 import LandmarkList from mediapipe.framework.formats.rect_pb2 import NormalizedRect from google.protobuf.json_format import MessageToJson import glob import re from pprint import pprint import json import sys if len(sys.argv) > 1: targetDir = sys.argv[1] else: targetDir = "./result/iPhoneXR_overshelf_scene1_short_hands8.mp4/" outputFiles = glob.glob(targetDir + "/" + "*.txt") detectionFiles = [(re.findall( r"iLoop=(\d+)_detection_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles] detectionFilesFiltered = [ (detectionFile[0], detectionFile[1].replace("\\", "/")) for detectionFile in detectionFiles if detectionFile[0]] landmarkFiles = [(re.findall( r"iLoop=(\d+)_landmark_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles] landmarkFilesFiltered = [ (landmarkFile[0], landmarkFile[1].replace("\\", "/")) for landmarkFile in landmarkFiles if landmarkFile[0]] handRectFiles = [(re.findall( r"iLoop=(\d+)_handRect_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles] handRectFilesFiltered = [ (handRectFile[0], handRectFile[1].replace("\\", "/")) for handRectFile in handRectFiles if handRectFile[0]] palmRectFiles = [(re.findall( r"iLoop=(\d+)_palmRect_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles] palmRectFilesFiltered = [ (palmRectFile[0], palmRectFile[1].replace("\\", "/")) for palmRectFile in palmRectFiles if palmRectFile[0]] handRectFromLandmarksFiles = [(re.findall( r"iLoop=(\d+)_handRectFromLandmarks_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles] handRectFromLandmarksFilesFiltered = [ (handRectFromLandmarksFile[0], handRectFromLandmarksFile[1].replace("\\", "/")) for handRectFromLandmarksFile in handRectFromLandmarksFiles if handRectFromLandmarksFile[0]] for detectionFile in detectionFilesFiltered: with open(detectionFile[1], "rb") as f: content = f.read() detection = Detection() detection.ParseFromString(content) jsonObj = MessageToJson(detection) detectionFileOutput = detectionFile[1].replace("txt", "json") with open(detectionFileOutput, "w") as f: f.write(jsonObj) for landmarkFile in landmarkFilesFiltered: with open(landmarkFile[1], "rb") as f: content = f.read() landmark = LandmarkList() landmark.ParseFromString(content) jsonObj = MessageToJson(landmark) landmarkFileOutput = landmarkFile[1].replace("txt", "json") with open(landmarkFileOutput, "w") as f: f.write(jsonObj) for handRectFile in handRectFilesFiltered: with open(handRectFile[1], "rb") as f: content = f.read() handRect = NormalizedRect() handRect.ParseFromString(content) jsonObj = MessageToJson(handRect) handRectFileOutput = handRectFile[1].replace("txt", "json") with open(handRectFileOutput, "w") as f: f.write(jsonObj) for palmRectFile in palmRectFilesFiltered: with open(palmRectFile[1], "rb") as f: content = f.read() palmRect = NormalizedRect() palmRect.ParseFromString(content) jsonObj = MessageToJson(palmRect) palmRectFileOutput = palmRectFile[1].replace("txt", "json") with open(palmRectFileOutput, "w") as f: f.write(jsonObj) for handRectFromLandmarksFile in handRectFromLandmarksFilesFiltered: with open(handRectFromLandmarksFile[1], "rb") as f: content = f.read() handRectFromLandmarks = NormalizedRect() handRectFromLandmarks.ParseFromString(content) jsonObj = MessageToJson(handRectFromLandmarks) handRectFromLandmarksFileOutput = handRectFromLandmarksFile[1].replace("txt", "json") with open(handRectFromLandmarksFileOutput, "w") as f: f.write(jsonObj)
Pythonで実行します。
python3 converter.py
結果
得られるデータは以下のようなものになります。
detection(Detection型)
検出した物体(=手、正確には手のひら/手の甲)のラベルやスコア、バウンディングボックス、手のひら内のキーポイントがわかります。
{ "label": [ "Palm" ], "score": [ 0.9865578 ], "locationData": { "format": "RELATIVE_BOUNDING_BOX", "relativeBoundingBox": { "xmin": 0.39248314, "ymin": 0.26998103, "width": 0.048939228, "height": 0.08700302 }, "relativeKeypoints": [ { "x": 0.40684253, "y": 0.2803954 }, { "x": 0.4169947, "y": 0.33548334 }, { "x": 0.4065402, "y": 0.34309858 }, { "x": 0.40017793, "y": 0.34611765 }, { "x": 0.3962021, "y": 0.34618986 }, { "x": 0.425763, "y": 0.27499962 }, { "x": 0.43927395, "y": 0.30067408 } ] } }
palmRect(NormalizedRect型)
palmRectという名前ですが、囲っているのは手全体になっています。
{ "xCenter": 0.416743, "yCenter": 0.35698244, "height": 0.22620799, "width": 0.12724198, "rotation": -3.1330206 }
handRect(NormalizedRect型)
{ "xCenter": 0.7210063, "yCenter": 0.1579529, "height": 0.1772464, "width": 0.0997011, "rotation": -2.8904288 }
landmark(Landmark型)
21点あります。手首で1点、各指で4点ずつあります。こちらのブログで画像を作成されている方がいらっしゃいます。
{ "landmark": [ { "x": 0.40840688, "y": 0.28062445, "z": 0.0 }, { "x": 0.42267725, "y": 0.276408, "z": 0.0 }, { "x": 0.43161613, "y": 0.29168004, "z": 0.0 }, { "x": 0.4361234, "y": 0.3206246, "z": 0.0 }, { "x": 0.44291618, "y": 0.35354337, "z": 0.0 }, { "x": 0.4092951, "y": 0.32624507, "z": 0.0 }, { "x": 0.4172063, "y": 0.3700544, "z": 0.0 }, { "x": 0.4325033, "y": 0.39400557, "z": 0.0 }, { "x": 0.44500858, "y": 0.4125882, "z": 0.0 }, { "x": 0.4057869, "y": 0.3467034, "z": 0.0 }, { "x": 0.42252448, "y": 0.38882673, "z": 0.0 }, { "x": 0.43710503, "y": 0.38357568, "z": 0.0 }, { "x": 0.4447874, "y": 0.37505886, "z": 0.0 }, { "x": 0.40684992, "y": 0.359745, "z": 0.0 }, { "x": 0.42386237, "y": 0.3922723, "z": 0.0 }, { "x": 0.43604085, "y": 0.38865924, "z": 0.0 }, { "x": 0.4427886, "y": 0.38379118, "z": 0.0 }, { "x": 0.41125774, "y": 0.36923084, "z": 0.0 }, { "x": 0.4223674, "y": 0.38892198, "z": 0.0 }, { "x": 0.43208086, "y": 0.38867816, "z": 0.0 }, { "x": 0.43905717, "y": 0.3841055, "z": 0.0 } ] }
handRectFromLandmark(NormalizedRect型)
{ "xCenter": 0.42539775, "yCenter": 0.3444981, "height": 0.21788836, "width": 0.12256221, "rotation": -3.071221 }
inputFrame, outputFrame(jpg形式ファイル)
注:Pythonで変換したものではなく、C++のプログラムで得られるものです。
まとめ
今回は、Graphとプログラムを変更し、Graph内の検出データを抽出し、ファイル出力したデータをPythonでJSON形式に変換しました。これによって、検出データを簡単に扱えるようになりました。
次回は、複数人に対応できるよう、3つ以上の手を検出してみたいと思います。
次↓
参考にさせていただいたページ
Protocol Buffer Basics: C++ https://developers.google.com/protocol-buffers/docs/cpptutorial
How to Install Latest Protobuf on Ubuntu 18.04 https://gist.github.com/diegopacheco/cd795d36e6ebcd2537cd18174865887b#file-latest-protobuf-ubuntu-18-04-md
PythonでProtocol Buffersの文字列と数値の単純なメッセージを操作する https://nansystem.com/protocol-buffers-in-python/
MediaPipe の Hand Tracking を mac で動かす https://qiita.com/mml/items/9128de32103f88b9927c
MediaPipe Visualizer https://viz.mediapipe.dev/